/**
 * Copyright (c) 2014, the Railo Company Ltd.
 * Copyright (c) 2015, Lucee Association Switzerland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either 
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package lucee.runtime.config;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import lucee.commons.digest.MD5;
import lucee.commons.io.IOUtil;
import lucee.commons.io.SystemUtil;
import lucee.commons.io.log.Log;
import lucee.commons.io.log.LogUtil;
import lucee.commons.io.res.Resource;
import lucee.commons.io.res.ResourcesImpl;
import lucee.commons.io.res.type.compress.CompressResource;
import lucee.commons.io.res.type.compress.CompressResourceProvider;
import lucee.commons.io.res.util.ResourceUtil;
import lucee.commons.lang.StringUtil;
import lucee.commons.net.URLDecoder;
import lucee.loader.engine.CFMLEngine;
import lucee.loader.engine.CFMLEngineFactory;
import lucee.runtime.CFMLFactoryImpl;
import lucee.runtime.Mapping;
import lucee.runtime.MappingImpl;
import lucee.runtime.PageContext;
import lucee.runtime.PageContextImpl;
import lucee.runtime.PageSource;
import lucee.runtime.PageSourceImpl;
import lucee.runtime.crypt.BlowfishEasy;
import lucee.runtime.engine.CFMLEngineImpl;
import lucee.runtime.engine.ThreadLocalPageContext;
import lucee.runtime.exp.ApplicationException;
import lucee.runtime.exp.PageException;
import lucee.runtime.exp.PageRuntimeException;
import lucee.runtime.exp.SecurityException;
import lucee.runtime.listener.ApplicationContext;
import lucee.runtime.listener.ApplicationListener;
import lucee.runtime.listener.ClassicAppListener;
import lucee.runtime.listener.MixedAppListener;
import lucee.runtime.listener.ModernAppListener;
import lucee.runtime.listener.NoneAppListener;
import lucee.runtime.monitor.Monitor;
import lucee.runtime.net.http.ReqRspUtil;
import lucee.runtime.net.http.ServletConfigDummy;
import lucee.runtime.net.http.ServletContextDummy;
import lucee.runtime.op.Caster;
import lucee.runtime.op.Decision;
import lucee.runtime.security.SecretProviderFactory;
import lucee.runtime.security.SecurityManager;
import lucee.runtime.type.Array;
import lucee.runtime.type.ArrayImpl;
import lucee.runtime.type.Collection;
import lucee.runtime.type.Collection.Key;
import lucee.runtime.type.KeyImpl;
import lucee.runtime.type.Struct;
import lucee.runtime.type.StructImpl;
import lucee.runtime.type.util.ArrayUtil;
import lucee.runtime.type.util.ListUtil;
import lucee.transformer.library.function.FunctionLib;
import lucee.transformer.library.tag.TagLib;

public final class ConfigUtil {

	private static String enckey;

	private static ConfigWebImpl dummyConfigWeb;

	/**
	 * default encryption for configuration (not very secure)
	 * 
	 * @param str
	 * @return
	 */
	public static CharSequence decrypt(CharSequence str) {
		if (str instanceof String) return str;
		return decrypt(str);
	}

	public static String decrypt(String str) {
		if (StringUtil.isEmpty(str) || !StringUtil.startsWithIgnoreCase(str, "encrypted:")) return str;
		str = str.substring(10);
		return new BlowfishEasy(getEncKey()).decryptString(str);
	}

	/**
	 * default encryption for configuration (not very secure)
	 * 
	 * @param str
	 * @return
	 */
	public static String encrypt(String str) {
		if (StringUtil.isEmpty(str)) return "";
		if (StringUtil.startsWithIgnoreCase(str, "encrypted:")) return str;
		return "encrypted:" + new BlowfishEasy(getEncKey()).encryptString(str);
	}

	private static String getEncKey() {
		if (enckey == null) {
			enckey = SystemUtil.getSystemPropOrEnvVar("lucee.password.enc.key", "sdfsdfs");
		}
		return enckey;
	}

	/**
	 * deploys all content in "web-deployment" to a web context, used for new context mostly or update
	 * existings
	 * 
	 * @param cs
	 * @param cw
	 * @param throwError
	 * @throws IOException
	 */
	public static void deployWeb(ConfigServer cs, ConfigWeb cw, boolean throwError) throws IOException {
		Resource deploy = cs.getConfigDir().getRealResource("web-deployment"), trg;
		if (!deploy.isDirectory()) return;

		trg = cw.getRootDirectory();
		try {
			_deploy(cw, deploy, trg);
		}
		catch (IOException ioe) {
			if (throwError) throw ioe;
			LogUtil.logGlobal(ThreadLocalPageContext.getConfig(cs), ConfigUtil.class.getName(), ioe);
		}
	}

	/**
	 * deploys all content in "web-context-deployment" to a web context, used for new context mostly or
	 * update existings
	 * 
	 * @param cs
	 * @param cw
	 * @param throwError
	 * @throws IOException
	 */
	public static void deployWebContext(ConfigServer cs, ConfigWeb cw, boolean throwError) throws IOException {
		Resource deploy = cs.getConfigDir().getRealResource("web-context-deployment"), trg;
		if (!deploy.isDirectory()) return;
		trg = cw.getConfigDir().getRealResource("context");
		try {
			_deploy(cw, deploy, trg);
		}
		catch (IOException ioe) {
			if (throwError) throw ioe;
			LogUtil.logGlobal(ThreadLocalPageContext.getConfig(cs != null ? cs : cw), ConfigAdmin.class.getName(), ioe);
		}
	}

	private static void _deploy(ConfigWeb cw, Resource src, Resource trg) throws IOException {
		if (!src.isDirectory()) return;
		if (trg.isFile()) trg.delete();
		if (!trg.exists()) trg.mkdirs();
		Resource _src, _trg;
		Resource[] children = src.listResources();
		if (ArrayUtil.isEmpty(children)) return;

		for (int i = 0; i < children.length; i++) {
			_src = children[i];
			_trg = trg.getRealResource(_src.getName());
			if (_src.isDirectory()) _deploy(cw, _src, _trg);
			if (_src.isFile()) {
				if (_src.length() != _trg.length()) {
					_src.copyTo(_trg, false);
					LogUtil.logGlobal(ThreadLocalPageContext.getConfig(cw), Log.LEVEL_DEBUG, ConfigUtil.class.getName(), "write file:" + _trg);

				}
			}
		}
	}

	/**
	 * touch a file object by the string definition
	 * 
	 * @param config
	 * @param directory
	 * @param path
	 * @param type
	 * @return matching file
	 */
	public static Resource getFile(Config config, Resource directory, String path, short type) {
		path = replacePlaceholder(path, config);

		Resource rel = directory.getRealResource(path);
		Resource abs = config.getResource(path);
		boolean isChildOf = abs.getParentResource().getParentResource() != null && ResourceUtil.isChildOf(abs, directory);

		for (short level: new short[] { ResourceUtil.LEVEL_PARENT_FILE, ResourceUtil.LEVEL_GRAND_PARENT_FILE, ResourceUtil.LEVEL_ALL }) {
			if (!StringUtil.isEmpty(path, true)) {

				if (!isChildOf) {
					Resource file = ResourceUtil.createResource(rel, level, type);
					if (file != null) return file;
				}
				Resource file = ResourceUtil.createResource(abs, level, type);
				if (file != null) return file;
			}
		}
		return null;
	}

	/**
	 * generate a file object by the string definition
	 * 
	 * @param rootDir
	 * @param strDir
	 * @param defaultDir
	 * @param configDir
	 * @param type
	 * @param config
	 * @return file
	 */
	static Resource getFile(Resource rootDir, String strDir, String defaultDir, Resource configDir, short type, short level, ConfigPro config) {
		strDir = replacePlaceholder(strDir, config);
		if (!StringUtil.isEmpty(strDir, true)) {
			Resource res, tmp;

			// non default resource
			if (startWithScheme(strDir)) {
				String scheme = getScheme(strDir, null);
				if (scheme != null && !scheme.equalsIgnoreCase(config.getDefaultResourceProvider().getScheme())) {
					res = ResourceUtil.createResource(config.getResource(strDir), level, type);
					if (res != null) return res;
				}
			}

			// create resource relative to rootDir
			else if (rootDir != null) {
				res = ResourceUtil.createResource(rootDir.getRealResource(strDir), level, type);
				if (res != null) return res;
			}

			// create resource absolute
			tmp = config.getResource(strDir);
			if (ResourceUtil.getExistingAncestorFolder(tmp, configDir) != null) {
				res = ResourceUtil.createResource(tmp, level, type);
				if (res != null) return res;
			}
		}
		if (defaultDir == null) return null;
		Resource file = ResourceUtil.createResource(configDir.getRealResource(defaultDir), level, type);
		return file;
	}

	public static boolean hasPlaceholder(String str) {
		if (StringUtil.isEmpty(str)) return false;
		// TOD improve test
		int index = str.indexOf('{');
		if (index > -1 && index < str.indexOf('}')) return true;
		return false;

	}

	// do not change, used in extension
	public static String replacePlaceholder(String str, Config config) {
		if (StringUtil.isEmpty(str)) return str;

		if (StringUtil.startsWith(str, '{')) {

			// Config Server
			if (str.startsWith("{lucee-config")) {
				if (str.startsWith("}", 13)) str = checkResult(str, config.getConfigDir().getReal(str.substring(14)));
				else if (str.startsWith("-dir}", 13)) str = checkResult(str, config.getConfigDir().getReal(str.substring(18)));
				else if (str.startsWith("-directory}", 13)) str = checkResult(str, config.getConfigDir().getReal(str.substring(24)));
				else if (str.startsWith("-file}", 13)) str = checkResult(str, config.getConfigFile().getReal(str.substring(19)));
			}

			else if (config != null && str.startsWith("{lucee-server")) {
				Resource dir = config instanceof ConfigWeb ? ((ConfigWeb) config).getConfigServerDir() : config.getConfigDir();
				// if(config instanceof ConfigServer && cs==null) cs=(ConfigServer) cw;
				if (dir != null) {
					if (str.startsWith("}", 13)) str = checkResult(str, dir.getReal(str.substring(14)));
					else if (str.startsWith("-dir}", 13)) str = checkResult(str, dir.getReal(str.substring(18)));
					else if (str.startsWith("-directory}", 13)) str = checkResult(str, dir.getReal(str.substring(24)));
				}
			}
			// Config Web
			else if (str.startsWith("{lucee-web")) {
				if (str.startsWith("}", 10)) str = checkResult(str, config.getConfigDir().getReal(str.substring(11)));
				else if (str.startsWith("-dir}", 10)) str = checkResult(str, config.getConfigDir().getReal(str.substring(15)));
				else if (str.startsWith("-directory}", 10)) str = checkResult(str, config.getConfigDir().getReal(str.substring(21)));
			}
			// Web Root
			else if (str.startsWith("{web-root")) {
				if (config instanceof ConfigWeb) {
					if (str.startsWith("}", 9)) str = checkResult(str, config.getRootDirectory().getReal(str.substring(10)));
					else if (str.startsWith("-dir}", 9)) str = checkResult(str, config.getRootDirectory().getReal(str.substring(14)));
					else if (str.startsWith("-directory}", 9)) str = checkResult(str, config.getRootDirectory().getReal(str.substring(20)));
				}
			}
			// Temp
			else if (str.startsWith("{temp")) {
				if (str.startsWith("}", 5)) str = checkResult(str, config.getTempDirectory().getRealResource(str.substring(6)).toString());
				else if (str.startsWith("-dir}", 5)) str = checkResult(str, config.getTempDirectory().getRealResource(str.substring(10)).toString());
				else if (str.startsWith("-directory}", 5)) str = checkResult(str, config.getTempDirectory().getRealResource(str.substring(16)).toString());
			}
			else if (config instanceof ServletConfig) {
				Map<String, String> labels = null;
				labels = ConfigUtil.getConfigServerImpl(config).getLabels();
				if (labels != null) str = SystemUtil.parsePlaceHolder(str, ((ServletConfig) config).getServletContext(), labels);
			}
			else str = SystemUtil.parsePlaceHolder(str);

			if (StringUtil.startsWith(str, '{')) {
				Struct constants = config.getConstants();
				if (constants != null) {
					Iterator<Entry<Key, Object>> it = constants.entryIterator();
					Entry<Key, Object> e;
					while (it.hasNext()) {
						e = it.next();
						if (StringUtil.startsWithIgnoreCase(str, "{" + e.getKey().getString() + "}")) {
							String value = (String) e.getValue();
							str = checkResult(str, config.getResource(value).getReal(str.substring(e.getKey().getString().length() + 2)));
							break;
						}
					}
				}
			}
		}
		return str;
	}

	private static String checkResult(String src, String res) {
		boolean srcEndWithSep = StringUtil.endsWith(src, ResourceUtil.FILE_SEPERATOR) || StringUtil.endsWith(src, '/') || StringUtil.endsWith(src, '\\');
		boolean resEndWithSep = StringUtil.endsWith(res, ResourceUtil.FILE_SEPERATOR) || StringUtil.endsWith(res, '/') || StringUtil.endsWith(res, '\\');
		if (srcEndWithSep && !resEndWithSep) return res + ResourceUtil.FILE_SEPERATOR;
		if (!srcEndWithSep && resEndWithSep) return res.substring(0, res.length() - 1);

		return res;
	}

	/**
	 * get only an existing file, dont create it
	 * 
	 * @param sc
	 * @param strDir
	 * @param configDir
	 * @param type
	 * @param config
	 * @param checkFromWebroot
	 * @param existing
	 * @return existing file
	 */
	public static Resource getResource(ServletContext sc, String strDir, Resource configDir, short type, Config config, boolean checkFromWebroot, boolean existing) {

		// ARP
		strDir = replacePlaceholder(strDir, config);
		// checkFromWebroot &&
		if (!StringUtil.isEmpty(strDir, true)) {
			Resource res, rel = null, abs = null;
			// looking for a match relative to the webroot, but only if there is no scheme
			if (sc != null && !startWithScheme(strDir)) {
				rel = config.getResource(ResourceUtil.merge(ReqRspUtil.getRootPath(sc), strDir));
				res = _getExistingFile(rel, type);
				if (res != null) return res;
			}
			// check for resource directly
			try {
				abs = config.getResource(strDir);
				res = _getExistingFile(abs, type);
				if (res != null) return res;
			}
			catch (Exception e) {
				// throws an exception if we have an invalid resource provider
				return null;
			}

			/// now we give no existing folder a chance
			if (!existing) {
				// if we have a non default resource provider or a parent folder exists
				if (abs != null && !config.getDefaultResourceProvider().getScheme().equals(abs.getResourceProvider().getScheme())
						|| ResourceUtil.getExistingAncestorFolder(abs, null) != null) {
					return abs;
				}
				// if the parent folder exist
				if (rel != null && ResourceUtil.doesParentExists(rel)) {
					return rel;
				}
			}
		}
		return null;

	}

	private static boolean startWithScheme(String path) {
		if (StringUtil.isEmpty(path, true)) return false;
		int index = path.indexOf("://");
		if (index == -1) return false;
		String scheme = path.substring(0, index);
		if (scheme.isEmpty() || !ResourcesImpl.onlyAlphaNumeric(scheme)) return false;

		return true;
	}

	private static String getScheme(String path, String defaultValue) {
		if (StringUtil.isEmpty(path, true)) return defaultValue;
		int index = path.indexOf("://");
		if (index == -1) return defaultValue;
		String scheme = path.substring(0, index);
		if (scheme.isEmpty() || !ResourcesImpl.onlyAlphaNumeric(scheme)) return defaultValue;

		return scheme;
	}

	private static Resource _getExistingFile(Resource file, short type) {

		boolean asDir = type == ResourceUtil.TYPE_DIR;
		// File
		if (file.exists() && ((file.isDirectory() && asDir) || (file.isFile() && !asDir))) {
			return ResourceUtil.getCanonicalResourceEL(file);
		}
		return null;
	}

	/**
	 * checks if file is a directory or not, if directory doesn't exist, it will be created
	 * 
	 * @param directory
	 * @return is directory or not
	 */
	public static boolean isDirectory(Resource directory) {
		if (directory.exists()) return directory.isDirectory();
		return directory.mkdirs();
	}

	/**
	 * checks if file is a file or not, if file doesn't exist, it will be created
	 * 
	 * @param file
	 * @return is file or not
	 */
	public static boolean isFile(Resource file) {
		if (file.exists()) return file.isFile();
		Resource parent = file.getParentResource();
		return parent.mkdirs() && file.createNewFile();
	}

	/**
	 * has access checks if config object has access to given type
	 * 
	 * @param config
	 * @param type
	 * @return has access
	 */
	public static boolean hasAccess(Config config, int type) {

		boolean has = true;
		if (config instanceof ConfigWeb) {
			has = config

					.getSecurityManager()

					.getAccess(type) != SecurityManager.VALUE_NO;
		}
		return has;
	}

	public static String translateOldPath(String path) {
		if (path == null) return path;
		if (path.startsWith("/WEB-INF/lucee/")) {
			path = "{web-root}" + path;
		}
		return path;
	}

	public static Object getIdMapping(Mapping m) {
		StringBuilder id = new StringBuilder(m.getVirtualLowerCase());
		if (m.hasPhysical()) id.append(m.getStrPhysical());
		if (m.hasArchive()) id.append(m.getStrPhysical());
		return m.toString().toLowerCase();
	}

	public static void checkGeneralReadAccess(ConfigPro config, Password password) throws SecurityException {
		SecurityManager sm = config.getSecurityManager();
		short access = sm.getAccess(SecurityManager.TYPE_ACCESS_READ);
		if (config instanceof ConfigServer) access = SecurityManager.ACCESS_PROTECTED;
		if (access == SecurityManager.ACCESS_PROTECTED) {
			checkPassword(config, "read", password);
		}
		else if (access == SecurityManager.ACCESS_CLOSE) {
			throw new SecurityException("can't access, read access is disabled");
		}
	}

	public static void checkGeneralWriteAccess(ConfigPro config, Password password) throws SecurityException {
		SecurityManager sm = config.getSecurityManager();
		if (sm == null) return;
		short access = sm.getAccess(SecurityManager.TYPE_ACCESS_WRITE);

		if (config instanceof ConfigServer) access = SecurityManager.ACCESS_PROTECTED;
		if (access == SecurityManager.ACCESS_PROTECTED) {
			checkPassword(config, "write", password);
		}
		else if (access == SecurityManager.ACCESS_CLOSE) {
			throw new SecurityException("can't access, write access is disabled");
		}
	}

	public static void checkPassword(ConfigPro config, String type, Password password) throws SecurityException {
		if (!config.hasPassword()) throw new SecurityException("can't access password protected information from the configuration, no password is defined for "
				+ (config instanceof ConfigServer ? "the server context" : "this web context")); // TODO make the message more clear for someone using the admin indirectly in
		// source code by using ACF specific interfaces
		if (!config.passwordEqual(password)) {
			if (StringUtil.isEmpty(password)) {
				if (type == null) throw new SecurityException("Access is protected",
						"to access the configuration without a password, you need to change the access to [open] in the Server Administrator");
				throw new SecurityException(type + " access is protected",
						"to access the configuration without a password, you need to change the " + type + " access to [open] in the Server Administrator");
			}
			throw new SecurityException("No access, password is invalid");
		}
	}

	public static String createMD5FromResource(Resource resource) throws IOException {
		InputStream is = null;
		try {
			is = resource.getInputStream();
			byte[] barr = IOUtil.toBytes(is);
			return MD5.getDigestAsString(barr);
		}
		finally {
			IOUtil.close(is);
		}
	}

	public static int toListenerMode(String strListenerMode, int defaultValue) {
		if (StringUtil.isEmpty(strListenerMode, true)) return defaultValue;
		strListenerMode = strListenerMode.trim();

		if ("current".equalsIgnoreCase(strListenerMode) || "curr".equalsIgnoreCase(strListenerMode)) return ApplicationListener.MODE_CURRENT;
		else if ("currenttoroot".equalsIgnoreCase(strListenerMode) || "current2root".equalsIgnoreCase(strListenerMode) || "curr2root".equalsIgnoreCase(strListenerMode)
				|| "modern".equalsIgnoreCase(strListenerMode)/* this is a patch for old version getting it wrong */)
			return ApplicationListener.MODE_CURRENT2ROOT;
		else if ("currentorroot".equalsIgnoreCase(strListenerMode) || "currorroot".equalsIgnoreCase(strListenerMode)) return ApplicationListener.MODE_CURRENT_OR_ROOT;
		else if ("root".equalsIgnoreCase(strListenerMode)) return ApplicationListener.MODE_ROOT;

		return defaultValue;
	}

	public static String toListenerMode(int listenerMode, String defaultValue) {
		if (ApplicationListener.MODE_CURRENT == listenerMode) return "current";
		else if (ApplicationListener.MODE_CURRENT2ROOT == listenerMode) return "curr2root";
		else if (ApplicationListener.MODE_CURRENT_OR_ROOT == listenerMode) return "currorroot";
		else if (ApplicationListener.MODE_ROOT == listenerMode) return "root";
		return defaultValue;
	}

	public static int toListenerType(String strListenerType, int defaultValue) {
		if (StringUtil.isEmpty(strListenerType, true)) return defaultValue;
		strListenerType = strListenerType.trim();

		if ("none".equalsIgnoreCase(strListenerType)) return ApplicationListener.TYPE_NONE;
		else if ("classic".equalsIgnoreCase(strListenerType)) return ApplicationListener.TYPE_CLASSIC;
		else if ("modern".equalsIgnoreCase(strListenerType) || "curr2root".equalsIgnoreCase(strListenerType)/* this is a patch for old version getting it wrong */)
			return ApplicationListener.TYPE_MODERN;
		else if ("mixed".equalsIgnoreCase(strListenerType)) return ApplicationListener.TYPE_MIXED;

		return defaultValue;
	}

	public static String toListenerType(int listenerType, String defaultValue) {
		if (ApplicationListener.TYPE_NONE == listenerType) return "none";
		else if (ApplicationListener.TYPE_CLASSIC == listenerType) return "classic";
		else if (ApplicationListener.TYPE_MODERN == listenerType) return "modern";
		else if (ApplicationListener.TYPE_MIXED == listenerType) return "mixed";

		return defaultValue;
	}

	public static ApplicationListener loadListener(String type, ApplicationListener defaultValue) {
		return loadListener(toListenerType(type, -1), defaultValue);
	}

	public static ApplicationListener loadListener(int type, ApplicationListener defaultValue) {
		// none
		if (ApplicationListener.TYPE_NONE == type) return new NoneAppListener();
		// classic
		if (ApplicationListener.TYPE_CLASSIC == type) return new ClassicAppListener();
		// modern
		if (ApplicationListener.TYPE_MODERN == type) return new ModernAppListener();
		// mixed
		if (ApplicationListener.TYPE_MIXED == type) return new MixedAppListener();

		return defaultValue;
	}

	public static short inspectTemplate(String str, short defaultValue) {
		if (str == null) return defaultValue;
		str = str.trim().toLowerCase();
		if (str.equals("auto")) return ConfigPro.INSPECT_AUTO;
		else if (str.equals("always")) return Config.INSPECT_ALWAYS;
		else if (str.equals("never")) return Config.INSPECT_NEVER;
		else if (str.equals("once")) return Config.INSPECT_ONCE;
		return defaultValue;
	}

	public static String inspectTemplate(short s, String defaultValue) {
		switch (s) {
		case ConfigPro.INSPECT_AUTO:
			return "auto";
		case Config.INSPECT_ALWAYS:
			return "always";
		case Config.INSPECT_NEVER:
			return "never";
		case Config.INSPECT_ONCE:
			return "once";
		default:
			return defaultValue;
		}
	}

	public static short toScopeCascading(String type, short defaultValue) {
		if (StringUtil.isEmpty(type)) return defaultValue;
		if (type.equalsIgnoreCase("strict")) return Config.SCOPE_STRICT;
		else if (type.equalsIgnoreCase("small")) return Config.SCOPE_SMALL;
		else if (type.equalsIgnoreCase("standard")) return Config.SCOPE_STANDARD;
		else if (type.equalsIgnoreCase("standart")) return Config.SCOPE_STANDARD;
		return defaultValue;
	}

	public static short toScopeCascading(boolean searchImplicitScopes) {
		if (searchImplicitScopes) return Config.SCOPE_STANDARD;
		return Config.SCOPE_STRICT;

	}

	public static String toScopeCascading(short type, String defaultValue) {
		switch (type) {
		case Config.SCOPE_STRICT:
			return "strict";
		case Config.SCOPE_SMALL:
			return "small";
		case Config.SCOPE_STANDARD:
			return "standard";
		default:
			return defaultValue;
		}
	}

	@Deprecated
	public static CFMLEngine getEngine(Config config) {
		if (config instanceof ConfigWeb) return ((ConfigWeb) config).getFactory().getEngine();
		if (config instanceof ConfigServer) return ((ConfigServer) config).getEngine();
		return CFMLEngineFactory.getInstance();
	}

	public static CFMLEngine getCFMLEngine(Config config) {
		if (config instanceof ConfigWeb) return ((ConfigWeb) config).getFactory().getEngine();
		if (config instanceof ConfigServer) return ((ConfigServer) config).getEngine();
		return CFMLEngineFactory.getInstance();
	}

	public static CFMLEngineFactory getCFMLEngineFactory(Config config) {
		try {
			if (config instanceof ConfigWeb) return ((ConfigWeb) config).getFactory().getEngine().getCFMLEngineFactory();
			if (config instanceof ConfigServer) return ((ConfigServer) config).getEngine().getCFMLEngineFactory();
			return CFMLEngineFactory.getInstance().getCFMLEngineFactory();
		}
		catch (RuntimeException e) {
			if (CFMLEngineImpl.FACTORY() != null) return CFMLEngineImpl.FACTORY();
			throw e;
		}
	}

	public static Resource getConfigServerDirectory(Config config) {
		if (config == null) config = ThreadLocalPageContext.getConfig();
		if (config instanceof ConfigWeb) return ((ConfigWeb) config).getConfigServerDir();
		if (config == null) return null;
		return (config).getConfigDir();
	}

	public static Mapping[] getAllMappings(PageContext pc) {
		List<Mapping> list = new ArrayList<Mapping>();
		getAllMappings(list, pc.getConfig().getMappings());
		getAllMappings(list, pc.getConfig().getCustomTagMappings());
		getAllMappings(list, pc.getConfig().getComponentMappings());
		getAllMappings(list, pc.getApplicationContext().getMappings());
		// MUST show all application contexts | also get component and custom tags mappings from application
		// context
		return list.toArray(new Mapping[list.size()]);
	}

	public static Mapping[] getAllMappings(Config cw) {
		List<Mapping> list = new ArrayList<Mapping>();
		getAllMappings(list, cw.getMappings());
		getAllMappings(list, cw.getCustomTagMappings());
		getAllMappings(list, cw.getComponentMappings());
		return list.toArray(new Mapping[list.size()]);
	}

	private static void getAllMappings(List<Mapping> list, Mapping[] mappings) {
		if (!ArrayUtil.isEmpty(mappings)) for (int i = 0; i < mappings.length; i++) {
			list.add(mappings[i]);
		}
	}

	public static int toMonitorType(String type, int defaultValue) {
		if (type == null) return defaultValue;

		type = type.trim();
		if ("request".equalsIgnoreCase(type)) return Monitor.TYPE_REQUEST;
		else if ("action".equalsIgnoreCase(type)) return Monitor.TYPE_ACTION;
		else if ("interval".equalsIgnoreCase(type) || "intervall".equalsIgnoreCase(type)) return Monitor.TYPE_INTERVAL;

		return defaultValue;
	}

	public static Mapping[] sort(Mapping[] mappings) {
		Arrays.sort(mappings, new Comparator() {
			@Override
			public int compare(Object left, Object right) {
				Mapping r = ((Mapping) right);
				Mapping l = ((Mapping) left);
				int rtn = r.getVirtualLowerCaseWithSlash().length() - l.getVirtualLowerCaseWithSlash().length();
				if (rtn == 0) return slashCount(r) - slashCount(l);
				return rtn;
			}

			private int slashCount(Mapping l) {
				String str = l.getVirtualLowerCaseWithSlash();
				int count = 0, lastIndex = -1;
				while ((lastIndex = str.indexOf('/', lastIndex)) != -1) {
					count++;
					lastIndex++;
				}
				return count;
			}
		});
		return mappings;
	}

	public static ConfigServer getConfigServer(Config config, Password password) throws PageException {
		if (config instanceof ConfigServer) return (ConfigServer) config;
		return ((ConfigWeb) config).getConfigServer(password);
	}

	protected static TagLib[] duplicate(TagLib[] tlds, boolean deepCopy) {
		TagLib[] rst = new TagLib[tlds.length];
		for (int i = 0; i < tlds.length; i++) {
			rst[i] = tlds[i].duplicate(deepCopy);
		}
		return rst;
	}

	protected static FunctionLib[] duplicate(FunctionLib[] flds, boolean deepCopy) {
		FunctionLib[] rst = new FunctionLib[flds.length];
		for (int i = 0; i < flds.length; i++) {
			rst[i] = flds[i].duplicate(deepCopy);
		}
		return rst;
	}

	public static Object replaceConfigPlaceHolders(Config config, Object obj) {
		if (obj == null) return obj;

		// handle simple value
		if (Decision.isSimpleValue(obj)) {
			if (obj instanceof CharSequence) return replaceConfigPlaceHolder(config, obj.toString());
			return obj;
		}

		// handle collection
		if (obj instanceof lucee.runtime.type.Collection) {
			return replaceConfigPlaceHolders(config, (lucee.runtime.type.Collection) obj);
		}

		return obj;
	}

	public static lucee.runtime.type.Collection replaceConfigPlaceHolders(Config config, lucee.runtime.type.Collection data) {
		if (data == null) return data;

		lucee.runtime.type.Collection repl;
		if (data instanceof Struct) repl = new StructImpl();
		else if (data instanceof Array) repl = new ArrayImpl();
		else return data;
		Iterator<Entry<Key, Object>> it = data.entryIterator();
		Entry<Key, Object> e;
		while (it.hasNext()) {
			e = it.next();
			repl.setEL(e.getKey(), replaceConfigPlaceHolders(config, e.getValue()));
		}
		return repl;
	}

	public static String replaceConfigPlaceHolderX(String v) {
		return replaceConfigPlaceHolder(null, v);
	}

	public final static String replaceConfigPlaceHolder(Config config, String v) {
		if (StringUtil.isEmpty(v) || v.indexOf('{') == -1) return v;

		int s = -1, e = -1, d = -1, sec = -1;
		int prefixLen, start = -1, end;
		String _name;
		CharSequence _prop;
		while ((s = v.indexOf("{system:", start)) != -1 | /* don't change */
				(e = v.indexOf("{env:", start)) != -1 | /* don't change */
				(d = v.indexOf("${", start)) != -1 | /* don't change */
				(sec = v.indexOf("{secret:", start)) != -1) {
			boolean isSystem = false, isDollar = false, isSecret = false;
			// system
			if (s > -1 && (e == -1 || e > s)) {
				start = s;
				prefixLen = 8;
				isSystem = true;
			}
			// env
			else if (e > -1) {
				start = e;
				prefixLen = 5;
			}
			// secret
			else if (sec > -1) {
				start = sec;
				prefixLen = 8;
				isSecret = true;
			}
			// dollar
			else {
				start = d;
				prefixLen = 2;
				isDollar = true;
			}

			end = v.indexOf('}', start);
			if (end > prefixLen) {
				_name = v.substring(start + prefixLen, end);
				// print.edate(_name);
				if (isSecret) {

					String[] _parts = _name.split(":");
					try {
						if (config == null) throw new ApplicationException("cannot use secret in this context");
						if (_parts.length == 1) {
							_prop = SecretProviderFactory.getSecret(ThreadLocalPageContext.getConfig(config), null, _parts[0], true);
						}
						else {
							_prop = SecretProviderFactory.getSecret(ThreadLocalPageContext.getConfig(config), _parts[0], _parts[1], true);
						}
					}
					catch (PageException pe) {
						throw new PageRuntimeException(pe);
					}
				}
				else if (isDollar) {
					String[] _parts = _name.split(":");
					_prop = SystemUtil.getSystemPropOrEnvVar(_parts[0], (_parts.length > 1) ? _parts[1] : null);
				}
				else {
					_prop = isSystem ? System.getProperty(_name) : System.getenv(_name);
				}

				if (_prop != null) {
					v = new StringBuilder().append(v.substring(0, start)).append(_prop).append(v.substring(end + 1)).toString();
					start += _prop.length();
				}
				else start = end;
			}
			else start = end; // set start to end for the next round
			s = -1;
			e = -1; // reset index
			d = -1; // I don't think we need this?
		}
		return v;
	}

	public static Array getAsArray(String parent, String child, Struct sct) {
		return getAsArray(child, getAsStruct(parent, sct));
	}

	public static Array getAsArray(Config config, Struct input, boolean replacePlaceholders, String... names) {
		Array arr = null;
		if (input == null) return arr;

		Object obj;
		for (String name: names) {
			obj = input.get(name, null);
			if (obj instanceof Array && ((arr = (Array) obj).size() > 0)) {
				break;
			}
		}

		if (arr == null) {
			arr = new ArrayImpl();
			input.put(names[0], arr);
			return arr;
		}
		return replacePlaceholders ? (Array) replaceConfigPlaceHolders(config, arr) : arr;
	}

	public static Struct getAsStruct(Config config, Struct input, boolean allowCSSString, String... names) {
		Struct sct = null;
		if (input == null) return sct;

		Object obj;
		for (String name: names) {
			obj = input.get(name, null);
			if (obj instanceof Struct && !(sct = (Struct) obj).isEmpty()) {
				break;
			}
		}

		if (allowCSSString && sct == null) {
			for (String name: names) {
				obj = input.get(name, null);
				if (obj instanceof CharSequence && !StringUtil.isEmpty(obj.toString(), true)) {
					sct = toStruct(config, obj.toString().trim());
					if (!sct.isEmpty()) break;
				}
			}

		}

		if (sct == null) {
			sct = new StructImpl(Struct.TYPE_LINKED);
			input.put(names[0], sct);
			return sct;
		}
		return (Struct) replaceConfigPlaceHolders(config, sct);
	}

	public static Struct toStruct(Config config, String str) {

		Struct sct = new StructImpl(StructImpl.TYPE_LINKED);
		try {
			String[] arr = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(str, '&'));
			String[] item;
			for (int i = 0; i < arr.length; i++) {
				item = ListUtil.toStringArray(ListUtil.listToArrayRemoveEmpty(arr[i], '='));
				if (item.length == 2) sct.setEL(KeyImpl.init(URLDecoder.decode(item[0], true).trim()), replaceConfigPlaceHolder(config, URLDecoder.decode(item[1], true)));
				else if (item.length == 1) sct.setEL(KeyImpl.init(URLDecoder.decode(item[0], true).trim()), "");
			}
		}
		catch (PageException ee) {
		}

		return sct;
	}

	// TODO
	/**
	 * @deprecated use instead getAsStruct(Struct input, String... names)
	 */
	@Deprecated
	public static Struct getAsStruct(String name, Struct sct) {
		Object obj = sct.get(name, null);
		if (obj == null) {
			Struct tmp = new StructImpl(Struct.TYPE_LINKED);
			sct.put(name, tmp);
			return tmp;
		}
		return (Struct) obj;
	}

	// TODO
	/**
	 * @deprecated use instead getAsArray(Struct input, String... names)
	 */
	@Deprecated
	public static Array getAsArray(String name, Struct sct) {
		Object obj = sct.get(KeyImpl.init(name), null);
		if (obj == null) {
			Array tmp = new ArrayImpl();
			sct.put(name, tmp);
			return tmp;
		}

		if (obj instanceof Array) return (Array) obj;

		Array tmp = new ArrayImpl();
		tmp.appendEL(obj);
		sct.put(name, tmp);
		return tmp;
	}

	/**
	 * get an array that matches oe of the given key or creates the array in place
	 * 
	 * @param input struct to look for the names
	 * @param convertStructToArray if true and the value is a struct, convert it to an array
	 * @param convertKey if the value is a struct the key of that strcut will be copied to the array
	 *            with that name
	 * @param stringKey in case the array contains string values and this key is set, create a struct
	 *            containing a key with the string as value
	 * @param names
	 * @return
	 * @throws PageException
	 */
	public static Array getAsArray(Config config, Struct input, boolean convertStructToArray, Key convertKey, Key stringKey, boolean replacePlaceHolder, String... names) {
		Array arr = null;
		if (input == null) return arr;

		Object obj;
		for (String name: names) {
			obj = input.get(KeyImpl.init(name), null);
			if (obj instanceof Array && (arr = (Array) obj).size() > 0) {
				if (name != names[0]) {
					input.setEL(KeyImpl.init(names[0]), arr);
					input.removeEL(KeyImpl.init(name));
				}
				break;
			}
			if (arr == null && convertStructToArray && obj instanceof Struct) {
				Struct sct = (Struct) obj;
				arr = new ArrayImpl();
				input.setEL(KeyImpl.init(name), arr);
				if (name != names[0]) {
					input.setEL(KeyImpl.init(names[0]), arr);
					input.removeEL(KeyImpl.init(name));
				}
				Iterator<Entry<Key, Object>> it = sct.entryIterator();
				Entry<Key, Object> e;
				Struct s;
				Object v;
				while (it.hasNext()) {
					e = it.next();
					v = e.getValue();
					if (convertKey != null && v instanceof Struct) {
						s = (Struct) v;
						if (!s.containsKey(convertKey)) s.setEL(convertKey, e.getKey().getString());
					}
					arr.appendEL(v);
				}
				break;
			}
		}

		// validate the array values
		if (arr != null && stringKey != null) {
			Key[] keys = arr.keys();
			Object val;
			for (Key k: keys) {
				val = arr.get(k, null);
				if (val instanceof CharSequence) {
					Struct sct = new StructImpl();
					sct.setEL(stringKey, val);
					arr.setEL(k, sct);
				}
			}
		}
		else if (arr == null) {
			arr = new ArrayImpl();
			input.setEL(KeyImpl.init(names[0]), arr);
			return arr;
		}

		if (replacePlaceHolder) return (Array) replaceConfigPlaceHolders(config, arr);

		return arr;
	}

	public static String getAsString(String name, Struct sct, String defaultValue) {
		if (sct == null) return defaultValue;
		Object obj = sct.get(KeyImpl.init(name), null);
		if (obj == null) return defaultValue;
		return Caster.toString(obj, defaultValue);
	}

	public static String getAsString(Collection.Key name, Struct sct, String defaultValue) {
		if (sct == null) return defaultValue;
		Object obj = sct.get(name, null);
		if (obj == null) return defaultValue;
		return Caster.toString(obj, defaultValue);
	}

	public static double getAsDouble(String name, Struct sct, double defaultValue) {
		if (sct == null) return defaultValue;
		Object obj = sct.get(KeyImpl.init(name), null);
		if (obj == null) return defaultValue;
		return Caster.toDoubleValue(obj, false, defaultValue);
	}

	public static Mapping getMapping(Config config, String virtual, Mapping defaultValue) {
		for (Mapping m: config.getMappings()) {
			if (m.getVirtualLowerCaseWithSlash().equalsIgnoreCase(virtual) || m.getVirtualLowerCase().equalsIgnoreCase(virtual)) return m;
		}
		return defaultValue;
	}

	public static PageSource[] getPageSources(PageContext pc, ConfigPro config, Mapping[] mappings, String realPath, boolean onlyTopLevel, boolean useSpecialMappings,
			boolean useDefaultMapping, boolean useComponentMappings, boolean onlyFirstMatch) {
		return (PageSource[]) getSources(pc, config, mappings, realPath, onlyTopLevel, useSpecialMappings, useDefaultMapping, useComponentMappings, onlyFirstMatch, true);
	}

	public static Resource[] getResources(PageContext pc, ConfigPro config, Mapping[] mappings, String realPath, boolean onlyTopLevel, boolean useSpecialMappings,
			boolean useDefaultMapping, boolean useComponentMappings, boolean onlyFirstMatch) {
		return (Resource[]) getSources(pc, config, mappings, realPath, onlyTopLevel, useSpecialMappings, useDefaultMapping, useComponentMappings, onlyFirstMatch, false);
	}

	/**
	 * get sources, can be PageSource or Resource based on "asPageSource" is true or false
	 */
	private static Object[] getSources(PageContext pc, ConfigPro config, Mapping[] mappings, String realPath, boolean onlyTopLevel, boolean useSpecialMappings,
			boolean useDefaultMapping, boolean useComponentMappings, boolean onlyFirstMatch, boolean asPageSource) {
		realPath = realPath.replace('\\', '/');
		String lcRealPath = StringUtil.toLowerCase(realPath) + '/';
		Mapping mapping;
		Mapping rootApp = null;
		PageSource ps;
		Resource res;
		List list = asPageSource ? new ArrayList<PageSource>() : new ArrayList<Resource>();

		if (mappings != null) {
			for (int i = 0; i < mappings.length; i++) {
				mapping = mappings[i];
				// we keep this for later
				if ("/".equals(mapping.getVirtual())) {
					rootApp = mapping;
					continue;
				}
				if (lcRealPath.startsWith(mapping.getVirtualLowerCaseWithSlash(), 0)) {
					if (asPageSource) {
						ps = mapping.getPageSource(realPath.substring(mapping.getVirtual().length()));
						if (onlyFirstMatch) return new PageSource[] { ps };
						else list.add(ps);
					}
					else {
						if (mapping instanceof MappingImpl) res = ((MappingImpl) mapping).getResource(realPath.substring(mapping.getVirtual().length()));
						else res = mapping.getPageSource(realPath.substring(mapping.getVirtual().length())).getResource();

						if (onlyFirstMatch) return new Resource[] { res };
						else list.add(res);
					}

				}
			}
		}

		/// special mappings
		if (useSpecialMappings && lcRealPath.startsWith("/mapping-", 0)) {
			String virtual = "/mapping-tag";

			// tag mappings

			Mapping[] tagMappings = (config instanceof ConfigWebPro)
					? new Mapping[] { ConfigUtil.getConfigServerImpl(config).getDefaultTagMapping(), config.getDefaultTagMapping() }
					: new Mapping[] { config.getDefaultTagMapping() };

			if (lcRealPath.startsWith(virtual, 0)) {
				for (int i = 0; i < tagMappings.length; i++) {
					if (asPageSource) {
						ps = tagMappings[i].getPageSource(realPath.substring(virtual.length()));
						if (ps.exists()) {
							if (onlyFirstMatch) return new PageSource[] { ps };
							else list.add(ps);
						}
					}
					else {
						if (tagMappings[i] instanceof MappingImpl) res = ((MappingImpl) tagMappings[i]).getResource(realPath.substring(virtual.length()));
						else res = tagMappings[i].getPageSource(realPath.substring(virtual.length())).getResource();
						if (res.exists()) {
							if (onlyFirstMatch) return new Resource[] { res };
							else list.add(res);
						}
					}
				}
			}

			// customtag mappings
			tagMappings = config.getCustomTagMappings();
			virtual = "/mapping-customtag";
			if (lcRealPath.startsWith(virtual, 0)) {
				for (int i = 0; i < tagMappings.length; i++) {
					if (asPageSource) {
						ps = tagMappings[i].getPageSource(realPath.substring(virtual.length()));
						if (ps.exists()) {
							if (onlyFirstMatch) return new PageSource[] { ps };
							else list.add(ps);
						}
					}
					else {
						if (tagMappings[i] instanceof MappingImpl) res = ((MappingImpl) tagMappings[i]).getResource(realPath.substring(virtual.length()));
						else res = tagMappings[i].getPageSource(realPath.substring(virtual.length())).getResource();
						if (res.exists()) {
							if (onlyFirstMatch) return new Resource[] { res };
							else list.add(res);
						}
					}
				}
			}
		}

		// component mappings (only used for gateway)
		if (useComponentMappings || (pc != null && ((PageContextImpl) pc).isGatewayContext())) {
			boolean isCFC = Constants.isComponentExtension(ResourceUtil.getExtension(realPath, null));
			if (isCFC) {
				Mapping[] cmappings = config.getComponentMappings();
				for (int i = 0; i < cmappings.length; i++) {
					if (asPageSource) {
						ps = cmappings[i].getPageSource(realPath);
						if (ps.exists()) {
							if (onlyFirstMatch) return new PageSource[] { ps };
							else list.add(ps);
						}
					}
					else {
						if (cmappings[i] instanceof MappingImpl) res = ((MappingImpl) cmappings[i]).getResource(realPath);
						else res = cmappings[i].getPageSource(realPath).getResource();
						if (res.exists()) {
							if (onlyFirstMatch) return new Resource[] { res };
							else list.add(res);
						}
					}
				}
			}
		}

		Mapping[] thisMappings = config.getMappings();

		// config mappings
		for (int i = 0; i < thisMappings.length - 1; i++) {
			mapping = thisMappings[i];
			if ((!onlyTopLevel || mapping.isTopLevel()) && lcRealPath.startsWith(mapping.getVirtualLowerCaseWithSlash(), 0)) {
				if (asPageSource) {
					ps = mapping.getPageSource(realPath.substring(mapping.getVirtual().length()));
					if (onlyFirstMatch) return new PageSource[] { ps };
					else list.add(ps);
				}
				else {
					if (mapping instanceof MappingImpl) res = ((MappingImpl) mapping).getResource(realPath.substring(mapping.getVirtual().length()));
					else res = mapping.getPageSource(realPath.substring(mapping.getVirtual().length())).getResource();

					if (onlyFirstMatch) return new Resource[] { res };
					else list.add(res);
				}
			}
		}

		if (useDefaultMapping) {
			if (rootApp != null) mapping = rootApp;
			else mapping = thisMappings[thisMappings.length - 1];
			if (asPageSource) {
				ps = mapping.getPageSource(realPath);
				if (onlyFirstMatch) return new PageSource[] { ps };
				else list.add(ps);
			}
			else {
				if (mapping instanceof MappingImpl) res = ((MappingImpl) mapping).getResource(realPath);
				else res = mapping.getPageSource(realPath).getResource();
				if (onlyFirstMatch) return new Resource[] { res };
				else list.add(res);
			}
		}
		if (asPageSource) return list.toArray(new PageSource[list.size()]);
		else return list.toArray(new Resource[list.size()]);
	}

	public static PageSource getPageSourceExisting(PageContext pc, ConfigPro config, Mapping[] mappings, String realPath, boolean onlyTopLevel, boolean useSpecialMappings,
			boolean useDefaultMapping, boolean onlyPhysicalExisting) {
		realPath = realPath.replace('\\', '/');
		String lcRealPath = StringUtil.toLowerCase(realPath) + '/';
		Mapping mapping;
		PageSource ps;
		Mapping rootApp = null;
		if (mappings != null) {
			for (int i = 0; i < mappings.length; i++) {
				mapping = mappings[i];
				// we keep this for later
				if ("/".equals(mapping.getVirtual())) {
					rootApp = mapping;
					continue;
				}
				if (lcRealPath.startsWith(mapping.getVirtualLowerCaseWithSlash(), 0)) {
					ps = mapping.getPageSource(realPath.substring(mapping.getVirtual().length()));
					if (onlyPhysicalExisting) {
						if (ps.physcalExists()) return ps;
					}
					else if (ps.exists()) return ps;
				}
			}
		}

		/// special mappings
		if (useSpecialMappings && lcRealPath.startsWith("/mapping-", 0)) {
			String virtual = "/mapping-tag";
			// tag mappings
			Mapping[] tagMappings = (config instanceof ConfigWebPro)
					? new Mapping[] { ConfigUtil.getConfigServerImpl(config).getDefaultTagMapping(), config.getDefaultTagMapping() }
					: new Mapping[] { config.getDefaultTagMapping() };
			if (lcRealPath.startsWith(virtual, 0)) {
				for (int i = 0; i < tagMappings.length; i++) {
					mapping = tagMappings[i];
					// if(lcRealPath.startsWith(mapping.getVirtualLowerCaseWithSlash(),0)) {
					ps = mapping.getPageSource(realPath.substring(virtual.length()));
					if (onlyPhysicalExisting) {
						if (ps.physcalExists()) return ps;
					}
					else if (ps.exists()) return ps;
					// }
				}
			}

			// customtag mappings
			tagMappings = config.getCustomTagMappings();
			virtual = "/mapping-customtag";
			if (lcRealPath.startsWith(virtual, 0)) {
				for (int i = 0; i < tagMappings.length; i++) {
					mapping = tagMappings[i];
					// if(lcRealPath.startsWith(mapping.getVirtualLowerCaseWithSlash(),0)) {
					ps = mapping.getPageSource(realPath.substring(virtual.length()));
					if (onlyPhysicalExisting) {
						if (ps.physcalExists()) return ps;
					}
					else if (ps.exists()) return ps;
					// }
				}
			}
		}

		// component mappings (only used for gateway)
		if (pc != null && ((PageContextImpl) pc).isGatewayContext()) {
			boolean isCFC = Constants.isComponentExtension(ResourceUtil.getExtension(realPath, null));
			if (isCFC) {
				Mapping[] cmappings = config.getComponentMappings();
				for (int i = 0; i < cmappings.length; i++) {
					ps = cmappings[i].getPageSource(realPath);
					if (onlyPhysicalExisting) {
						if (ps.physcalExists()) return ps;
					}
					else if (ps.exists()) return ps;
				}
			}
		}
		Mapping[] thisMappings = config.getMappings();

		// config mappings
		for (int i = 0; i < thisMappings.length - 1; i++) {
			mapping = thisMappings[i];
			if ((!onlyTopLevel || mapping.isTopLevel()) && lcRealPath.startsWith(mapping.getVirtualLowerCaseWithSlash(), 0)) {
				ps = mapping.getPageSource(realPath.substring(mapping.getVirtual().length()));
				if (onlyPhysicalExisting) {
					if (ps.physcalExists()) return ps;
				}
				else if (ps.exists()) return ps;
			}
		}

		if (useDefaultMapping) {
			if (rootApp != null) mapping = rootApp;
			else mapping = thisMappings[thisMappings.length - 1];

			ps = mapping.getPageSource(realPath);
			if (onlyPhysicalExisting) {
				if (ps.physcalExists()) return ps;
			}
			else if (ps.exists()) return ps;
		}
		return null;
	}

	public static PageSource toComponentPageSource(PageContext pc, Resource res, PageSource defaultValue) {
		String path;
		ApplicationContext ac = pc.getApplicationContext();
		if (ac != null) {
			Mapping[] mappings = ac.getComponentMappings();
			if (mappings != null) {
				for (Mapping m: mappings) {
					// Physical
					if (m.hasPhysical()) {
						path = ResourceUtil.getPathToChild(res, m.getPhysical());
						if (path != null) {
							return m.getPageSource(path);
						}
					}
					// Archive
					if (m.hasArchive() && res.getResourceProvider() instanceof CompressResourceProvider) {
						Resource archive = m.getArchive();
						CompressResource cr = ((CompressResource) res);
						if (archive.equals(cr.getCompressResource())) {
							return m.getPageSource(cr.getCompressPath());
						}
					}
				}
			}
		}
		Mapping[] mappings = pc.getConfig().getComponentMappings();
		if (mappings != null) {
			for (Mapping m: mappings) {
				// Physical
				if (m.hasPhysical()) {
					path = ResourceUtil.getPathToChild(res, m.getPhysical());
					if (path != null) {
						return m.getPageSource(path);
					}
				}
				// Archive
				if (m.hasArchive() && res.getResourceProvider() instanceof CompressResourceProvider) {
					Resource archive = m.getArchive();
					CompressResource cr = ((CompressResource) res);
					if (archive.equals(cr.getCompressResource())) {
						return m.getPageSource(cr.getCompressPath());
					}
				}
			}
		}

		return defaultValue;
	}

	public static PageSource toPageSource(ConfigPro config, Mapping[] mappings, Resource res, PageSource defaultValue) {
		Mapping mapping;
		String path;

		// app mappings
		if (mappings != null) {
			for (int i = 0; i < mappings.length; i++) {
				mapping = mappings[i];

				// Physical
				if (mapping.hasPhysical()) {
					path = ResourceUtil.getPathToChild(res, mapping.getPhysical());
					if (path != null) {
						return mapping.getPageSource(path);
					}
				}
				// Archive
				if (mapping.hasArchive() && res.getResourceProvider() instanceof CompressResourceProvider) {
					Resource archive = mapping.getArchive();
					CompressResource cr = ((CompressResource) res);
					if (archive.equals(cr.getCompressResource())) {
						return mapping.getPageSource(cr.getCompressPath());
					}
				}
			}
		}
		Mapping[] thisMappings = config.getMappings();
		// config mappings
		for (int i = 0; i < thisMappings.length; i++) {
			mapping = thisMappings[i];

			// Physical
			if (mapping.hasPhysical()) {
				path = ResourceUtil.getPathToChild(res, mapping.getPhysical());
				if (path != null) {
					return mapping.getPageSource(path);
				}
			}
			// Archive
			if (mapping.hasArchive() && res.getResourceProvider() instanceof CompressResourceProvider) {
				Resource archive = mapping.getArchive();
				CompressResource cr = ((CompressResource) res);
				if (archive.equals(cr.getCompressResource())) {
					return mapping.getPageSource(cr.getCompressPath());
				}
			}
		}

		// map resource to root mapping when same filesystem
		Mapping rootMapping = thisMappings[thisMappings.length - 1];
		Resource root;
		if (rootMapping.hasPhysical() && res.getResourceProvider().getScheme().equals((root = rootMapping.getPhysical()).getResourceProvider().getScheme())) {

			String realpath = "";
			while (root != null && !ResourceUtil.isChildOf(res, root)) {
				root = root.getParentResource();
				realpath += "../";
			}
			String p2c = ResourceUtil.getPathToChild(res, root);
			if (StringUtil.startsWith(p2c, '/') || StringUtil.startsWith(p2c, '\\')) p2c = p2c.substring(1);
			realpath += p2c;

			return rootMapping.getPageSource(realpath);

		}
		// MUST better impl than this
		if (config instanceof ConfigWebPro) {
			Resource parent = res.getParentResource();
			if (parent != null && !parent.equals(res)) {
				Mapping m = ((ConfigWebPro) config).getApplicationMapping("application", "/", parent.getAbsolutePath(), null, true, false);
				return m.getPageSource(res.getName());
			}
		}

		// Archive
		// MUST check archive
		return defaultValue;
	}

	public static List<PageSource> toAllLoadedPageSource(ConfigPro config, Mapping[] mappings, Resource res) {
		Mapping mapping;
		String path;
		List<PageSource> results = new ArrayList<>();
		PageSource ps;
		// app mappings
		if (mappings != null) {
			for (int i = 0; i < mappings.length; i++) {
				mapping = mappings[i];
				if (mapping.hasPhysical()) {
					path = ResourceUtil.getPathToChild(res, mapping.getPhysical());
					if (path != null) {
						ps = mapping.getPageSource(path);
						if (ps instanceof PageSourceImpl && ((PageSourceImpl) ps).isLoad()) {
							results.add(ps);
							continue;
						}

					}
				}
			}
		}
		Mapping[] thisMappings = config.getMappings();
		// config mappings
		for (int i = 0; i < thisMappings.length; i++) {
			mapping = thisMappings[i];
			if (mapping.hasPhysical()) {
				path = ResourceUtil.getPathToChild(res, mapping.getPhysical());
				if (path != null) {
					ps = mapping.getPageSource(path);
					if (ps instanceof PageSourceImpl && ((PageSourceImpl) ps).isLoad()) {
						results.add(ps);
						continue;
					}
				}
			}
		}

		// map resource to root mapping when same filesystem
		Mapping rootMapping = thisMappings[thisMappings.length - 1];
		Resource root;
		if (rootMapping.hasPhysical() && res.getResourceProvider().getScheme().equals((root = rootMapping.getPhysical()).getResourceProvider().getScheme())) {

			String realpath = "";
			while (root != null && !ResourceUtil.isChildOf(res, root)) {
				root = root.getParentResource();
				realpath += "../";
			}
			String p2c = ResourceUtil.getPathToChild(res, root);
			if (StringUtil.startsWith(p2c, '/') || StringUtil.startsWith(p2c, '\\')) p2c = p2c.substring(1);
			realpath += p2c;
			ps = rootMapping.getPageSource(realpath);
			if (ps instanceof PageSourceImpl && ((PageSourceImpl) ps).isLoad()) {
				results.add(ps);
			}
		}
		// MUST better impl than this
		if (config instanceof ConfigWebPro) {
			Resource parent = res.getParentResource();
			if (parent != null && !parent.equals(res)) {
				Mapping m = ((ConfigWebPro) config).getApplicationMapping("application", "/", parent.getAbsolutePath(), null, true, false);
				ps = m.getPageSource(res.getName());
				if (ps instanceof PageSourceImpl && ((PageSourceImpl) ps).isLoad()) {
					results.add(ps);
				}
			}
		}
		return results;
	}

	public static ConfigWeb toConfigWeb(Config config) throws PageException {
		if (config instanceof ConfigWeb) return (ConfigWeb) config;

		// do we have a config web in context
		Config c = ThreadLocalPageContext.getConfig();
		if (c instanceof ConfigWeb) return (ConfigWeb) config;
		if (config == null) config = c;

		// TODO better solution for this
		// pick one
		if (config instanceof ConfigServer) {
			ConfigWeb[] webs = ((ConfigServerImpl) c).getConfigWebs();
			if (webs != null && webs.length > 0) return webs[0];
			return getDummyConfigWeb((ConfigServer) config);
		}

		throw new ApplicationException("could not provide a ConfigWeb based on the given [" + config.getClass().getName() + "]");
		// Config config, Resource root, Struct attributes, Struct parameters, int majorVersion, int
		// minorVersion

	}

	public static ConfigWeb getDummyConfigWeb(ConfigServer cs) throws PageException {
		if (dummyConfigWeb == null) {
			ServletConfigDummy sc = new ServletConfigDummy(new ServletContextDummy(cs, SystemUtil.getSystemDirectory(), new StructImpl(), new StructImpl(), 0, 0), "dummy");
			CFMLFactoryImpl factory = new CFMLFactoryImpl((CFMLEngineImpl) cs.getEngine(), sc);
			dummyConfigWeb = new ConfigWebImpl(factory, (ConfigServerImpl) cs, sc);
		}
		return dummyConfigWeb;
	}

	public static class CacheElement {

		public final long created;
		public final PageSource pageSource;
		public final boolean isCFC;

		public CacheElement(PageSource pageSource, boolean isCFC) {
			this.created = System.currentTimeMillis();
			this.pageSource = pageSource;
			this.isCFC = isCFC;
		}

	}

	public static ConfigServerImpl getConfigServerImpl(Config config) {
		if (config instanceof ConfigServerImpl) return (ConfigServerImpl) config;
		if (config instanceof ConfigWebImpl) return ((ConfigWebImpl) config).getConfigServerImpl();
		// MUST remove
		throw new RuntimeException("getConfigServerImpl: " + config.getClass().getName());
	}

	/**
	 * returns ConfigWeb if available, otherwise ConfigServer
	 * 
	 * @param config
	 * @return
	 */
	public static ConfigPro getConfigWebIfPossible(Config config) {
		if (config instanceof ConfigWebImpl) return ((ConfigWebImpl) config);
		Config c = ThreadLocalPageContext.getConfig();
		if (c instanceof ConfigWebImpl) return ((ConfigWebImpl) c);

		return getConfigServerImpl(config);
	}
}